1 /*
2  * jQuery Autocomplete plugin
1.1
3  *
4  * Copyright (c)
2009 Jörn Zaefferer
5  *
6  * Dual licensed under the MIT and GPL licenses:
7  * http://www.opensource.org/licenses/mit-license.php
8  * http://www.gnu.org/licenses/gpl.html
9  *
10  * Revision: $Id: jquery.autocomplete.js
15 2009-08-22 10:30:27Z joern.zaefferer $
11  */

12
13 ;(function($) {
14     
15 $.fn.extend({
16     autocomplete: function(urlOrData, options) {
17         
var isUrl = typeof urlOrData == "string";
18         options = $.extend({}, $.Autocompleter.defaults, {
19             url: isUrl ? urlOrData :
null,
20             data: isUrl ?
null : urlOrData,
21             delay: isUrl ? $.Autocompleter.defaults.delay :
10,
22             max: options && !options.scroll ?
10 : 150
23         }, options);
24         
25         
// if highlight is set to false, replace it with a do-nothing function
26         options.highlight = options.highlight || function(
value) { return value; };
27         
28         
// if the formatMatch option is not specified, then use formatItem for backwards compatibility
29         options.formatMatch = options.formatMatch || options.formatItem;
30         
31         
return this.each(function() {
32             
new $.Autocompleter(this, options);
33         });
34     },
35     result: function(handler) {
36         
return this.bind("result", handler);
37     },
38     search: function(handler) {
39         
return this.trigger("search", [handler]);
40     },
41     flushCache: function() {
42         
return this.trigger("flushCache");
43     },
44     setOptions: function(options){
45         
return this.trigger("setOptions", [options]);
46     },
47     unautocomplete: function() {
48         
return this.trigger("unautocomplete");
49     }
50 });
51
52 $.Autocompleter = function(input, options) {
53
54     
var KEY = {
55         UP:
38,
56         DOWN:
40,
57         DEL:
46,
58         TAB:
9,
59         RETURN:
13,
60         ESC:
27,
61         COMMA:
188,
62         PAGEUP:
33,
63         PAGEDOWN:
34,
64         BACKSPACE:
8
65     };
66
67     
// Create $ object for input element
68     
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
69
70     
var timeout;
71     
var previousValue = "";
72     
var cache = $.Autocompleter.Cache(options);
73     
var hasFocus = 0;
74     
var lastKeyPressCode;
75     
var config = {
76         mouseDownOnSelect:
false
77     };
78     
var select = $.Autocompleter.Select(options, input, selectCurrent, config);
79     
80     
var blockSubmit;
81     
82     
// prevent form submit in opera when selecting with return key
83     $.browser.opera && $(input.form).bind(
"submit.autocomplete", function() {
84         
if (blockSubmit) {
85             blockSubmit =
false;
86             
return false;
87         }
88     });
89     
90     
// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
91     $input.bind(($.browser.opera ?
"keypress" : "keydown") + ".autocomplete", function(event) {
92         
// a keypress means the input has focus
93         
// avoids issue where input had focus before the autocomplete was applied
94         hasFocus =
1;
95         
// track last key pressed
96         lastKeyPressCode =
event.keyCode;
97         
switch(event.keyCode) {
98         
99             
case KEY.UP:
100                 
event.preventDefault();
101                 
if ( select.visible() ) {
102                     
select.prev();
103                 }
else {
104                     onChange(
0, true);
105                 }
106                 
break;
107                 
108             
case KEY.DOWN:
109                 
event.preventDefault();
110                 
if ( select.visible() ) {
111                     
select.next();
112                 }
else {
113                     onChange(
0, true);
114                 }
115                 
break;
116                 
117             
case KEY.PAGEUP:
118                 
event.preventDefault();
119                 
if ( select.visible() ) {
120                     
select.pageUp();
121                 }
else {
122                     onChange(
0, true);
123                 }
124                 
break;
125                 
126             
case KEY.PAGEDOWN:
127                 
event.preventDefault();
128                 
if ( select.visible() ) {
129                     
select.pageDown();
130                 }
else {
131                     onChange(
0, true);
132                 }
133                 
break;
134             
135             
// matches also semicolon
136             
case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
137             
case KEY.TAB:
138             
case KEY.RETURN:
139                 
if( selectCurrent() ) {
140                     
// stop default to prevent a form submit, Opera needs special handling
141                     
event.preventDefault();
142                     blockSubmit =
true;
143                     
return false;
144                 }
145                 
break;
146                 
147             
case KEY.ESC:
148                 
select.hide();
149                 
break;
150                 
151             
default:
152                 clearTimeout(timeout);
153                 timeout = setTimeout(onChange, options.delay);
154                 
break;
155         }
156     }).focus(function(){
157         
// track whether the field has focus, we shouldn't process any
158         
// results if the field no longer has focus
159         hasFocus++;
160     }).blur(function() {
161         hasFocus =
0;
162         
if (!config.mouseDownOnSelect) {
163             hideResults();
164         }
165     }).click(function() {
166         
// show select when clicking in a focused field
167         
if ( hasFocus++ > 1 && !select.visible() ) {
168             onChange(
0, true);
169         }
170     }).bind(
"search", function() {
171         
// TODO why not just specifying both arguments?
172         
var fn = (arguments.length > 1) ? arguments[1] : null;
173         function findValueCallback(q, data) {
174             
var result;
175             
if( data && data.length ) {
176                 
for (var i=0; i < data.length; i++) {
177                     
if( data[i].result.toLowerCase() == q.toLowerCase() ) {
178                         result = data[i];
179                         
break;
180                     }
181                 }
182             }
183             
if( typeof fn == "function" ) fn(result);
184             
else $input.trigger("result", result && [result.data, result.value]);
185         }
186         $.each(trimWords($input.val()), function(i,
value) {
187             request(
value, findValueCallback, findValueCallback);
188         });
189     }).bind(
"flushCache", function() {
190         cache.flush();
191     }).bind(
"setOptions", function() {
192         $.extend(options, arguments[
1]);
193         
// if we've updated the data, repopulate
194         
if ( "data" in arguments[1] )
195             cache.populate();
196     }).bind(
"unautocomplete", function() {
197         
select.unbind();
198         $input.unbind();
199         $(input.form).unbind(
".autocomplete");
200     });
201     
202     
203     function selectCurrent() {
204         
var selected = select.selected();
205         
if( !selected )
206             
return false;
207         
208         
var v = selected.result;
209         previousValue = v;
210         
211         
if ( options.multiple ) {
212             
var words = trimWords($input.val());
213             
if ( words.length > 1 ) {
214                 
var seperator = options.multipleSeparator.length;
215                 
var cursorAt = $(input).selection().start;
216                 
var wordAt, progress = 0;
217                 $.each(words, function(i, word) {
218                     progress += word.length;
219                     
if (cursorAt <= progress) {
220                         wordAt = i;
221                         
return false;
222                     }
223                     progress += seperator;
224                 });
225                 words[wordAt] = v;
226                 
// TODO this should set the cursor to the right position, but it gets overriden somewhere
227                 
//$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
228                 v = words.
join( options.multipleSeparator );
229             }
230             v += options.multipleSeparator;
231         }
232         
233         $input.val(v);
234         hideResultsNow();
235         $input.trigger(
"result", [selected.data, selected.value]);
236         
return true;
237     }
238     
239     function onChange(crap, skipPrevCheck) {
240         
if( lastKeyPressCode == KEY.DEL ) {
241             
select.hide();
242             
return;
243         }
244         
245         
var currentValue = $input.val();
246         
247         
if ( !skipPrevCheck && currentValue == previousValue )
248             
return;
249         
250         previousValue = currentValue;
251         
252         currentValue = lastWord(currentValue);
253         
if ( currentValue.length >= options.minChars) {
254             $input.addClass(options.loadingClass);
255             
if (!options.matchCase)
256                 currentValue = currentValue.toLowerCase();
257             request(currentValue, receiveData, hideResultsNow);
258         }
else {
259             stopLoading();
260             
select.hide();
261         }
262     };
263     
264     function trimWords(
value) {
265         
if (!value)
266             
return [""];
267         
if (!options.multiple)
268             
return [$.trim(value)];
269         
return $.map(value.split(options.multipleSeparator), function(word) {
270             
return $.trim(value).length ? $.trim(word) : null;
271         });
272     }
273     
274     function lastWord(
value) {
275         
if ( !options.multiple )
276             
return value;
277         
var words = trimWords(value);
278         
if (words.length == 1)
279             
return words[0];
280         
var cursorAt = $(input).selection().start;
281         
if (cursorAt == value.length) {
282             words = trimWords(
value)
283         }
else {
284             words = trimWords(
value.replace(value.substring(cursorAt), ""));
285         }
286         
return words[words.length - 1];
287     }
288     
289     
// fills in the input box w/the first match (assumed to be the best match)
290     
// q: the term entered
291     
// sValue: the first matching result
292     function autoFill(q, sValue){
293         
// autofill in the complete box w/the first match as long as the user hasn't entered in more data
294         
// if the last user key pressed was backspace, don't autofill
295         
if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
296             
// fill in the value (keep the case the user has typed)
297             $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
298             
// select the portion of the value not typed by the user (so the next character will erase)
299             $(input).selection(previousValue.length, previousValue.length + sValue.length);
300         }
301     };
302
303     function hideResults() {
304         clearTimeout(timeout);
305         timeout = setTimeout(hideResultsNow,
200);
306     };
307
308     function hideResultsNow() {
309         
var wasVisible = select.visible();
310         
select.hide();
311         clearTimeout(timeout);
312         stopLoading();
313         
if (options.mustMatch) {
314             
// call search and run callback
315             $input.search(
316                 function (result){
317                     
// if no value found, clear the input box
318                     
if( !result ) {
319                         
if (options.multiple) {
320                             
var words = trimWords($input.val()).slice(0, -1);
321                             $input.val( words.
join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
322                         }
323                         
else {
324                             $input.val(
"" );
325                             $input.trigger(
"result", null);
326                         }
327                     }
328                 }
329             );
330         }
331     };
332
333     function receiveData(q, data) {
334         
if ( data && data.length && hasFocus ) {
335             stopLoading();
336             
select.display(data, q);
337             autoFill(q, data[
0].value);
338             
select.show();
339         }
else {
340             hideResultsNow();
341         }
342     };
343
344     function request(term, success, failure) {
345         
if (!options.matchCase)
346             term = term.toLowerCase();
347         
var data = cache.load(term);
348         
// recieve the cached data
349         
if (data && data.length) {
350             success(term, data);
351         
// if an AJAX url has been supplied, try loading the data now
352         }
else if( (typeof options.url == "string") && (options.url.length > 0) ){
353             
354             
var extraParams = {
355                 timestamp: +
new Date()
356             };
357             $.each(options.extraParams, function(key, param) {
358                 extraParams[key] =
typeof param == "function" ? param() : param;
359             });
360             
361             $.ajax({
362                 
// try to leverage ajaxQueue plugin to abort previous requests
363                 mode:
"abort",
364                 
// limit abortion to this input
365                 port:
"autocomplete" + input.name,
366                 dataType: options.dataType,
367                 url: options.url,
368                 data: $.extend({
369                     q: lastWord(term),
370                     limit: options.max
371                 }, extraParams),
372                 success: function(data) {
373                     
var parsed = options.parse && options.parse(data) || parse(data);
374                     cache.
add(term, parsed);
375                     success(term, parsed);
376                 }
377             });
378         }
else {
379             
// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
380             
select.emptyList();
381             failure(term);
382         }
383     };
384     
385     function parse(data) {
386         
var parsed = [];
387         
var rows = data.split("\n");
388         
for (var i=0; i < rows.length; i++) {
389             
var row = $.trim(rows[i]);
390             
if (row) {
391                 row = row.split(
"|");
392                 parsed[parsed.length] = {
393                     data: row,
394                     
value: row[0],
395                     result: options.formatResult && options.formatResult(row, row[
0]) || row[0]
396                 };
397             }
398         }
399         
return parsed;
400     };
401
402     function stopLoading() {
403         $input.removeClass(options.loadingClass);
404     };
405
406 };
407
408 $.Autocompleter.defaults = {
409     inputClass:
"ac_input",
410     resultsClass:
"ac_results",
411     loadingClass:
"ac_loading",
412     minChars:
1,
413     delay:
400,
414     matchCase:
false,
415     matchSubset:
true,
416     matchContains:
false,
417     cacheLength:
10,
418     max:
100,
419     mustMatch:
false,
420     extraParams: {},
421     selectFirst:
true,
422     formatItem: function(row) {
return row[0]; },
423     formatMatch:
null,
424     autoFill:
false,
425     width:
0,
426     multiple:
false,
427     multipleSeparator:
", ",
428     highlight: function(
value, term) {
429         
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
430     },
431     scroll:
true,
432     scrollHeight:
180
433 };
434
435 $.Autocompleter.Cache = function(options) {
436
437     
var data = {};
438     
var length = 0;
439     
440     function matchSubset(s, sub) {
441         
if (!options.matchCase)
442             s = s.toLowerCase();
443         
var i = s.indexOf(sub);
444         
if (options.matchContains == "word"){
445             i = s.toLowerCase().search(
"\\b" + sub.toLowerCase());
446         }
447         
if (i == -1) return false;
448         
return i == 0 || options.matchContains;
449     };
450     
451     function
add(q, value) {
452         
if (length > options.cacheLength){
453             flush();
454         }
455         
if (!data[q]){
456             length++;
457         }
458         data[q] =
value;
459     }
460     
461     function populate(){
462         
if( !options.data ) return false;
463         
// track the matches
464         
var stMatchSets = {},
465             nullData =
0;
466
467         
// no url was specified, we need to adjust the cache length to make sure it fits the local data store
468         
if( !options.url ) options.cacheLength = 1;
469         
470         
// track all options for minChars = 0
471         stMatchSets[
""] = [];
472         
473         
// loop through the array and create a lookup structure
474         
for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
475             
var rawValue = options.data[i];
476             
// if rawValue is a string, make an array otherwise just reference the array
477             rawValue = (
typeof rawValue == "string") ? [rawValue] : rawValue;
478             
479             
var value = options.formatMatch(rawValue, i+1, options.data.length);
480             
if ( value === false )
481                 
continue;
482                 
483             
var firstChar = value.charAt(0).toLowerCase();
484             
// if no lookup array for this character exists, look it up now
485             
if( !stMatchSets[firstChar] )
486                 stMatchSets[firstChar] = [];
487
488             
// if the match is a string
489             
var row = {
490                 
value: value,
491                 data: rawValue,
492                 result: options.formatResult && options.formatResult(rawValue) ||
value
493             };
494             
495             
// push the current match into the set list
496             stMatchSets[firstChar].push(row);
497
498             
// keep track of minChars zero items
499             
if ( nullData++ < options.max ) {
500                 stMatchSets[
""].push(row);
501             }
502         };
503
504         
// add the data items to the cache
505         $.each(stMatchSets, function(i,
value) {
506             
// increase the cache size
507             options.cacheLength++;
508             
// add to the cache
509             
add(i, value);
510         });
511     }
512     
513     
// populate any existing data
514     setTimeout(populate,
25);
515     
516     function flush(){
517         data = {};
518         length =
0;
519     }
520     
521     
return {
522         flush: flush,
523         
add: add,
524         populate: populate,
525         load: function(q) {
526             
if (!options.cacheLength || !length)
527                 
return null;
528             
/*
529              *
if dealing w/local data and matchContains than we must make sure
530              * to loop through all the data collections looking
for matches
531              */

532             
if( !options.url && options.matchContains ){
533                 
// track all matches
534                 
var csub = [];
535                 
// loop through all the data grids for matches
536                 
for( var k in data ){
537                     
// don't search through the stMatchSets[""] (minChars: 0) cache
538                     
// this prevents duplicates
539                     
if( k.length > 0 ){
540                         
var c = data[k];
541                         $.each(c, function(i, x) {
542                             
// if we've got a match, add it to the array
543                             
if (matchSubset(x.value, q)) {
544                                 csub.push(x);
545                             }
546                         });
547                     }
548                 }
549                 
return csub;
550             }
else
551             
// if the exact item exists, use it
552             
if (data[q]){
553                 
return data[q];
554             }
else
555             
if (options.matchSubset) {
556                 
for (var i = q.length - 1; i >= options.minChars; i--) {
557                     
var c = data[q.substr(0, i)];
558                     
if (c) {
559                         
var csub = [];
560                         $.each(c, function(i, x) {
561                             
if (matchSubset(x.value, q)) {
562                                 csub[csub.length] = x;
563                             }
564                         });
565                         
return csub;
566                     }
567                 }
568             }
569             
return null;
570         }
571     };
572 };
573
574 $.Autocompleter.Select = function (options, input,
select, config) {
575     
var CLASSES = {
576         ACTIVE:
"ac_over"
577     };
578     
579     
var listItems,
580         active = -
1,
581         data,
582         term =
"",
583         needsInit =
true,
584         element,
585         list;
586     
587     
// Create results
588     function init() {
589         
if (!needsInit)
590             
return;
591         element = $(
"<div/>")
592         .hide()
593         .addClass(options.resultsClass)
594         .css(
"position", "absolute")
595         .appendTo(document.body);
596     
597         list = $(
"<ul/>").appendTo(element).mouseover( function(event) {
598             
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
599                 active = $(
"li", list).removeClass(CLASSES.ACTIVE).index(target(event));
600                 $(target(
event)).addClass(CLASSES.ACTIVE);
601             }
602         }).click(function(
event) {
603             $(target(
event)).addClass(CLASSES.ACTIVE);
604             
select();
605             
// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
606             input.focus();
607             
return false;
608         }).mousedown(function() {
609             config.mouseDownOnSelect =
true;
610         }).mouseup(function() {
611             config.mouseDownOnSelect =
false;
612         });
613         
614         
if( options.width > 0 )
615             element.css(
"width", options.width);
616             
617         needsInit =
false;
618     }
619     
620     function target(
event) {
621         
var element = event.target;
622         
while(element && element.tagName != "LI")
623             element = element.parentNode;
624         
// more fun with IE, sometimes event.target is empty, just ignore it then
625         
if(!element)
626             
return [];
627         
return element;
628     }
629
630     function moveSelect(step) {
631         listItems.slice(active, active +
1).removeClass(CLASSES.ACTIVE);
632         movePosition(step);
633         
var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
634         
if(options.scroll) {
635             
var offset = 0;
636             listItems.slice(
0, active).each(function() {
637                 offset +=
this.offsetHeight;
638             });
639             
if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
640                 list.scrollTop(offset + activeItem[
0].offsetHeight - list.innerHeight());
641             }
else if(offset < list.scrollTop()) {
642                 list.scrollTop(offset);
643             }
644         }
645     };
646     
647     function movePosition(step) {
648         active += step;
649         
if (active < 0) {
650             active = listItems.size() -
1;
651         }
else if (active >= listItems.size()) {
652             active =
0;
653         }
654     }
655     
656     function limitNumberOfItems(available) {
657         
return options.max && options.max < available
658             ? options.max
659             : available;
660     }
661     
662     function fillList() {
663         list.empty();
664         
var max = limitNumberOfItems(data.length);
665         
for (var i=0; i < max; i++) {
666             
if (!data[i])
667                 
continue;
668             
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
669             
if ( formatted === false )
670                 
continue;
671             
var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
672             $.data(li,
"ac_data", data[i]);
673         }
674         listItems = list.find(
"li");
675         
if ( options.selectFirst ) {
676             listItems.slice(
0, 1).addClass(CLASSES.ACTIVE);
677             active =
0;
678         }
679         
// apply bgiframe if available
680         
if ( $.fn.bgiframe )
681             list.bgiframe();
682     }
683     
684     
return {
685         display: function(d, q) {
686             init();
687             data = d;
688             term = q;
689             fillList();
690         },
691         next: function() {
692             moveSelect(
1);
693         },
694         prev: function() {
695             moveSelect(-
1);
696         },
697         pageUp: function() {
698             
if (active != 0 && active - 8 < 0) {
699                 moveSelect( -active );
700             }
else {
701                 moveSelect(-
8);
702             }
703         },
704         pageDown: function() {
705             
if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
706                 moveSelect( listItems.size() -
1 - active );
707             }
else {
708                 moveSelect(
8);
709             }
710         },
711         hide: function() {
712             element && element.hide();
713             listItems && listItems.removeClass(CLASSES.ACTIVE);
714             active = -
1;
715         },
716         visible : function() {
717             
return element && element.is(":visible");
718         },
719         current: function() {
720             
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
721         },
722         show: function() {
723             
var offset = $(input).offset();
724             element.css({
725                 width:
typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
726                 top: offset.top + input.offsetHeight,
727                 left: offset.left
728             }).show();
729             
if(options.scroll) {
730                 list.scrollTop(
0);
731                 list.css({
732                     maxHeight: options.scrollHeight,
733                     overflow:
'auto'
734                 });
735                 
736                 
if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
737                     
var listHeight = 0;
738                     listItems.each(function() {
739                         listHeight +=
this.offsetHeight;
740                     });
741                     
var scrollbarsVisible = listHeight > options.scrollHeight;
742                     list.css(
'height', scrollbarsVisible ? options.scrollHeight : listHeight );
743                     
if (!scrollbarsVisible) {
744                         
// IE doesn't recalculate width when scrollbar disappears
745                         listItems.width( list.width() - parseInt(listItems.css(
"padding-left")) - parseInt(listItems.css("padding-right")) );
746                     }
747                 }
748                 
749             }
750         },
751         selected: function() {
752             
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
753             
return selected && selected.length && $.data(selected[0], "ac_data");
754         },
755         emptyList: function (){
756             list && list.empty();
757         },
758         unbind: function() {
759             element && element.
remove();
760         }
761     };
762 };
763
764 $.fn.selection = function(start, end) {
765     
if (start !== undefined) {
766         
return this.each(function() {
767             
if( this.createTextRange ){
768                 
var selRange = this.createTextRange();
769                 
if (end === undefined || start == end) {
770                     selRange.move(
"character", start);
771                     selRange.
select();
772                 }
else {
773                     selRange.collapse(
true);
774                     selRange.moveStart(
"character", start);
775                     selRange.moveEnd(
"character", end);
776                     selRange.
select();
777                 }
778             }
else if( this.setSelectionRange ){
779                 
this.setSelectionRange(start, end);
780             }
else if( this.selectionStart ){
781                 
this.selectionStart = start;
782                 
this.selectionEnd = end;
783             }
784         });
785     }
786     
var field = this[0];
787     
if ( field.createTextRange ) {
788         
var range = document.selection.createRange(),
789             orig = field.
value,
790             teststring =
"<->",
791             textLength = range.text.length;
792         range.text = teststring;
793         
var caretAt = field.value.indexOf(teststring);
794         field.
value = orig;
795         
this.selection(caretAt, caretAt + textLength);
796         
return {
797             start: caretAt,
798             end: caretAt + textLength
799         }
800     }
else if( field.selectionStart !== undefined ){
801         
return {
802             start: field.selectionStart,
803             end: field.selectionEnd
804         }
805     }
806 };
807
808 })(jQuery);


Gõ tìm kiếm nhanh...